﻿/*
 * Copyright 2009-2014 Edward L. Stauff.           Contact: <EdStauff@gmail.com>.
 * 
 * This file, "ErrorUtils.cs", is part of the "Stauffware.Common" class library.
 * "Stauffware.Common" is free software: you can redistribute it and/or 
 * modify it under the terms of the GNU Lesser Public License as published by the 
 * Free Software Foundation, either version 3 of the License, or (at your option) 
 * any later version.
 * 
 * "Stauffware.Common" is distributed in the hope that it will be useful, 
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser Public License for more 
 * details.
 * 
 * You should have received a copy of the GNU Lesser Public License along with 
 * the "Stauffware.Common" sources.  If not, see <http://www.gnu.org/licenses/>.
 */
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Diagnostics;
using System.Text;
using System.IO;

namespace Stauffware.Common.Core
{
	/// <summary>
	/// Utility functions for manipulating Exceptions and other errors.
	/// </summary>
	public static class ErrorUtils
	{
		//=========================================================================
		/// <summary>
		/// Returns a string containing the message for the given exception,
		/// followed by the messages for any inner exceptions, recursively.
		/// </summary>
		/// <remarks>
		/// Does not include stack traces or debug data.  For complete exception
		/// information, use <see cref="ErrorUtils.DumpExceptionChain(Exception)"/>.
		/// </remarks>
		/// <param name="x">the exception</param>
		/// <param name="separator">what to put between exception messages</param>
		/// <returns>the exception chain information</returns>

		public static string GetMessageChain (Exception x, string separator)
		{
			string retStr = "";

			for (; ; )
			{
				retStr += x.Message;
				x = x.InnerException;
				if (x == null)
					return retStr;
				retStr += separator;
			}
		}

		//=========================================================================
		/// <summary>
		/// Dumps complete exception information for an exception chain in plain text format,
		/// including stack dumps, exception data, and properties of the exceptions.
		/// </summary>
		/// <param name="x">the exception to dump</param>
		/// <returns>the dumped information</returns>

		public static string DumpExceptionChain (Exception x)
		{
			StringBuilder sb = new StringBuilder();
			DumpExceptionChain(x, sb);
			return sb.ToString();
		}

		//=========================================================================
		/// <summary>
		/// Dumps complete exception information for an exception chain in plain text format,
		/// including stack dumps, exception data, and properties of the exceptions.
		/// </summary>
		/// <param name="x">the exception to dump</param>
		/// <param name="sb">where to dump the information</param>

		public static void DumpExceptionChain (Exception x, StringBuilder sb)
		{
			DumpExceptionChain(x, new StringWriter(sb));
		}

		//=========================================================================
		/// <summary>
		/// Dumps complete exception information for an exception chain in plain text format,
		/// including stack dumps, exception data, and properties of the exceptions.
		/// </summary>
		/// <param name="x">the exception to dump</param>
		/// <param name="tw">where to dump the information</param>
		/// <returns>the dumped information</returns>

		public static void DumpExceptionChain (Exception x, TextWriter tw)
		{
			for (int i = 0; x != null; x = x.InnerException)
			{
				if (i > 0)
					tw.WriteLine("Inner Exception (level " + i.ToString() + ")");
				DumpOneException(x, tw);
			}
		}

		//=========================================================================
		/// <summary>
		/// Dumps complete exception information for one exception in plain text format,
		/// including a stack dump, exception data, and properties of the exception.
		/// </summary>

		public static void DumpOneException (Exception x, TextWriter tw)
		{
			tw.WriteLine(x.GetType().FullName);
			tw.WriteLine("\tMessage = " + x.Message);
#if !SILVERLIGHT
			tw.WriteLine("\tSource = " + x.Source);
			tw.WriteLine("\tHelpLink = " + x.HelpLink);
#endif

			// dump the public instance properties (except the ones defined in System.Exception)
			foreach (PropertyInfo pi in x.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance))
			{
				if (pi.DeclaringType != typeof(Exception))
					DumpData("\t" + pi.Name, pi.GetValue(x, null), tw);
			}

			// dump the public instance fields (except the ones defined in System.Exception)
			foreach (FieldInfo fi in x.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance))
			{
				if (fi.DeclaringType != typeof(Exception))
					DumpData("\t" + fi.Name, fi.GetValue(x), tw);
			}

			if (x.Data != null && x.Data.Count > 0)
			{
				tw.WriteLine("\tData:");
				foreach (object key in x.Data.Keys)
					DumpData("\t\t" + key.ToString(), x.Data[key], tw);
			}

			StackTrace stack = new StackTrace(x, true);

			for (int i = 0; i < stack.FrameCount; i++)
			{
				StackFrame frame = stack.GetFrame(i);

				tw.Write("\t[" + i.ToString() + "] ");

				MethodBase method = frame.GetMethod();
				if (method != null)
				{
					if (method.DeclaringType != null)
						tw.Write(method.DeclaringType.FullName + ".");
					tw.Write(method.Name);

					ListString prams = new ListString(", ");
					foreach (ParameterInfo pi in method.GetParameters())
					{
						string type = pi.ParameterType.FullName;
						if (type == null)		// yes, this really happened (IEnumerator)
							type = pi.ParameterType.Name;
						prams.Add(type + " " + pi.Name);
					}

					tw.Write(" (" + prams.ToString() + ")  ");
				}

				tw.Write("<il+" + frame.GetILOffset().ToString() + ", n+" +
									frame.GetNativeOffset().ToString() + ">");

				if (!string.IsNullOrEmpty(frame.GetFileName()))
				{
					tw.WriteLine();
					tw.Write("\t    " + frame.GetFileName() + " line " +
								frame.GetFileLineNumber().ToString() + " col " +
								frame.GetFileColumnNumber().ToString());
				}

				if (method == null && string.IsNullOrEmpty(frame.GetFileName()))
					tw.Write("(no symbolic information available)");

				tw.WriteLine();
			}
		}

		//=========================================================================
		// helper for DumpOneException

		private static void DumpData (string name, object value, TextWriter tw)
		{
			tw.Write(name);
			tw.Write(" = ");
			tw.Write(ValueToString(value));
			tw.WriteLine();
		}

		//=========================================================================
		/// <summary>
		/// Converts a value to a human-readable string, for error reporting and
		/// debugging purposes.  
		/// This method will never return null, and should never throw an exception.
		/// </summary>
		/// <param name="value">the value to convert to a string</param>
		/// <returns>the human-readable string</returns>

		public static string ValueToString (object value)
		{
			if (value == null)
				return "NULL";
			
			if (value is string)
				return CharEncoding.ToCsharpLiteral((string)value);

			if (value is char)
			{
				char c = (char) value;
				return CharEncoding.ToCsharpLiteral(c) + 
						" (0x" + ((int)c).ToString("X2") + ")";
			}

			Type type = value.GetType();

			if (typeof(IEnumerable).IsAssignableFrom(type))		// or has GetEnumerator()
			{
				ListString members = new ListString("{ ", ", ", " }", "EMPTY");
				foreach (object member in (IEnumerable) value)
					members.Add(ValueToString(member));
				return members.ToString();
			}

			if (type.IsPrimitiveInteger())
			{
				string str = value.ToString();
				if (str.Length > 1)
				{
					string hex = ValueToHex(value);
					if (hex != null)
						return value.ToString() + " (0x" + hex + ")";
				}
				return str;
			}

			try
			{
				string str = value.ToString();	// example: System.DBNull.ToString() returns an empty string
				if (string.IsNullOrWhiteSpace(str))
					return value.GetType().FullName;
				else return str;
			}
			catch (Exception x)
			{
				// A rare case of throwing away an exception.
				// We assume that, in places where this method is used,
				// any exception returned by value.ToString() is less important
				// than returning a string that's at least marginally useful.
				return value.GetType().FullName + " (.ToString() threw " + GetMessageChain(x, " | ") + ")";
			}
		}

		// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

		// Renders the given value in hex.  Returns null on error.
		private static string ValueToHex (object value)
		{
			try
			{
				MethodInfo mi = value.GetType().GetMethod("ToString", new Type[] { typeof(string) });
				if (mi != null)
					return (string) mi.Invoke(value, new object[] { "X2" });
			}
			catch (Exception)
			{
				// Another rare case of throwing away an exception.
				return value.GetType().FullName + " (undisplayable)";
			}

			return null;
		}

		//=========================================================================
	}
}
